package cn.myperf4j.core;

import cn.myperf4j.base.MethodTag;
import cn.myperf4j.base.config.ProfilingConfig;
import cn.myperf4j.base.metric.MethodMetrics;
import cn.myperf4j.base.util.Logger;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * Created by LinShunkang on 2019/07/23
 */
public final class MethodMetricsHistogram {

    private static final ConcurrentMap<Integer, MethodMetricsInfo> METHOD_MAP = new ConcurrentHashMap<>(8 * 1024);

    private MethodMetricsHistogram() {
        //empty
    }

    public static void recordMetrics(MethodMetrics metrics) {
        recordMetrics0(metrics.getMethodTagId(),
                metrics.getTP95(),
                metrics.getTP99(),
                metrics.getTP999(),
                metrics.getTP9999());
    }

    public static void recordNoneMetrics(int methodTagId) {
        recordMetrics0(methodTagId, -1, -1, -1, -1);
    }

    private static void recordMetrics0(int methodTagId, int tp95, int tp99, int tp999, int tp9999) {
        final MethodMetricsInfo methodMetricsInfo = METHOD_MAP.get(methodTagId);
        if (methodMetricsInfo != null) {
            methodMetricsInfo.add(tp95, tp99, tp999, tp9999);
            return;
        }

        METHOD_MAP.put(methodTagId, new MethodMetricsInfo(tp95, tp99, tp999, tp9999));
    }

    public static void buildSysGenProfilingFile() {
        final long startMills = System.currentTimeMillis();
        final String filePath = ProfilingConfig.basicConfig().sysProfilingParamsFile();
        final String tempFilePath = filePath + "_tmp";
        final File tempFile = new File(tempFilePath);
        try (BufferedWriter fileWriter = new BufferedWriter(new FileWriter(tempFile, false), 8 * 1024)) {
            fileWriter.write("#This is a file automatically generated by MyPerf4J, please do not edit!\n");

            final List<Integer> neverInvokedMethods = new ArrayList<>(128);
            final MethodTagMaintainer tagMaintainer = MethodTagMaintainer.getInstance();
            for (Entry<Integer, MethodMetricsInfo> entry : METHOD_MAP.entrySet()) {
                final Integer methodId = entry.getKey();
                final MethodMetricsInfo info = entry.getValue();
                if (info.getCount() <= 0) {
                    neverInvokedMethods.add(methodId);
                    continue;
                }

                final int mostTimeThreshold = calMostTimeThreshold(info);
                writeProfilingInfo(tagMaintainer, fileWriter, methodId, mostTimeThreshold);
            }
            fileWriter.flush();

            if (!neverInvokedMethods.isEmpty()) {
                fileWriter.write("#The following methods have never been invoked!\n");
                for (final Integer methodId : neverInvokedMethods) {
                    writeProfilingInfo(tagMaintainer, fileWriter, methodId, 128);
                }
                fileWriter.flush();
            }

            final File destFile = new File(filePath);
            final boolean rename = tempFile.renameTo(destFile) && destFile.setReadOnly();
            Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile(): rename " + tempFile.getName()
                    + " to " + destFile.getName() + " " + (rename ? "success" : "fail"));
        } catch (Exception e) {
            Logger.error("MethodMetricsHistogram.buildSysGenProfilingFile()", e);
        } finally {
            Logger.debug("MethodMetricsHistogram.buildSysGenProfilingFile() finished, cost="
                    + (System.currentTimeMillis() - startMills) + "ms");
        }
    }

    private static void writeProfilingInfo(MethodTagMaintainer tagMaintainer,
                                           BufferedWriter fileWriter,
                                           Integer methodId,
                                           int mostTimeThreshold) throws IOException {
        final MethodTag methodTag = tagMaintainer.getMethodTag(methodId);
        if (methodTag == null) {
            Logger.warn("MethodMetricsHistogram.writeProfilingInfo(): methodId=" + methodId + ", methodTag is null");
            return;
        }

        fileWriter.write(methodTag.getFullDesc());
        fileWriter.write('=');
        fileWriter.write(mostTimeThreshold + ":" + calOutThresholdCount(mostTimeThreshold));
        fileWriter.newLine();
    }

    private static int calMostTimeThreshold(MethodMetricsInfo info) {
        final int count = info.getCount();
        final long tp9999Avg = info.getTp9999Sum() / count;
        if (tp9999Avg <= 64) {
            return 64;
        } else if (tp9999Avg <= 128) {
            return 128;
        } else if (tp9999Avg <= 256) {
            return 256;
        }

        final long tp999Avg = info.getTp999Sum() / count;
        if (tp999Avg <= 128) {
            return 128;
        } else if (tp999Avg <= 256) {
            return 256;
        } else if (tp999Avg <= 512) {
            return 512;
        }

        final long tp99Avg = info.getTp99Sum() / count;
        if (tp99Avg <= 256) {
            return 256;
        } else if (tp99Avg <= 512) {
            return 512;
        } else if (tp99Avg <= 1024) {
            return 1024;
        }

        final long tp95Avg = info.getTp95Sum() / count;
        if (tp95Avg <= 512) {
            return 512;
        } else if (tp95Avg <= 1024) {
            return 1024;
        } else if (tp95Avg <= 1536) {
            return 1536;
        }
        return 2048;
    }

    private static int calOutThresholdCount(int mostTimeThreshold) {
        if (mostTimeThreshold <= 256) {
            return 64;
        } else if (mostTimeThreshold <= 512) {
            return 128;
        } else if (mostTimeThreshold <= 1024) {
            return 256;
        } else if (mostTimeThreshold <= 1536) {
            return 512;
        } else {
            return 1024;
        }
    }
}
